module interpreter_sql;

import std.conv;
import std.regex;
import std.string;
import std.algorithm;
import core.exception;

import std.stdio;

import interpreter;
import database;
import storage;
import table;

class SQLInterpreter : Interpreter
{
  this() {
    keywords = [
      "quit":true, "q":true, "create":true, "insert":true, "into":true,
      "drop":true, "use":true, "alter":true, "select":true, "where":true,
      "delete":true, "from":true, "all":true, "ignore":true, "table":true,
      "temporary":true, "if":true, "not":true, "exists":true, "default":true,
      "int":true, "bool":true, "real":true, "string":true, "optional":true,
      "database":true, "values":true, "add":true,
    ];
  }

  bool repl(string function() reader, void function(string) writer) {
    bool quit = false;
		
    while(!quit) {
      try
      {
        writer("SQL :> ");
        quit = !parse(reader());
      }
      catch(Exception e)
      {
        writer(e.msg ~ "\n");
      }
    }
    return true;
  }

  bool   parse(string input) {
    auto tokens = tokenize(input);

    // Case insensitivity for keywords
    foreach(token; tokens) {
      if (toLower(token) in keywords) {
        token = toLower(token);
      }
    }

    try {
    switch(tokens[0]) {
      case "quit": case "q":
        if (tokens.length > 1)
          throw new Exception("Too many parameters.");
        if (db) stor.save(db);
        return false;

      case "use":
        if (tokens.length > 2)
          throw new Exception("Too many parameters.");
        if (db) {
          stor.save(db);
          delete db;
        }
        db = new Database(tokens[1]);
        if (stor.exists(db))
          stor.load(db);
        else {
          delete db;
          throw new Exception("The database '" ~ tokens[1] ~ "' does not exist!");
        }
        break;
      
      case "create":
        if (tokens[1] == "database") {
          if (tokens.length > 3)
            throw new Exception("Too many parameters.");
          db = new Database(tokens[2]);
        } else if (tokens[1] == "table") {
          if (!db)
            throw new Exception("There is no open database to operate on.");
          auto t = new Table(tokens[2]);
          string name, type;
          Header h;
          HeaderState state = HeaderState.Typed;
          bool optional = false;
          bool done = false;

          foreach(int i, string entry; tokens[3..$]) {
            final switch (state) {
              case HeaderState.Typed:
                if (entry == "optional") {
                  optional = true;
                } else {
                  if (done) {
                    t.addColumn(new Column(new Header(
                      name ~ "|" ~ type ~ "|" ~ (optional?"true":"false"))));
                    done = false;
                  }
                  optional = false;
                  name = entry;
                  state = HeaderState.Named;
                }
                break;
              case HeaderState.Named:
                type = entry;
                state = HeaderState.Typed;
                done = true;
                break;
            }
          }
          if (done) {
            t.addColumn(new Column(new Header(
              name ~ "|" ~ type ~ "|" ~ (optional?"true":"false"))));
          }
          db.addTable(t);
        } else
          throw new Exception("DATABASE or TABLE keyword expected.");
        break;

      case "drop":
        if (tokens[1] == "database") {
          if (db) { stor.remove(db); delete db; }
          else throw new Exception("There is no database open");
        } else if (tokens[1] == "table") {
          if (db.getTable(tokens[2]))
            db.removeTable(tokens[2]);
          else
            throw new Exception("There is no table named '" ~ tokens[2] ~ "'.");
        } else {
          throw new Exception("DATABASE or TABLE keyword expected.");
        }
        break;

      case "alter":
        if (!db)
          throw new Exception("There is no open database to operate on.");
        if (tokens[1] != "table")
          throw new Exception("The keyword TABLE expected after ALTER.");
        Table t = db.getTable(tokens[2]);
        if (!t)
           throw new Exception("There is no table named '" ~ tokens[2] ~ "'.");
        if (tokens[3] == "add") {
          t.addColumn(new Column(new Header(
            tokens[4] ~ "|" ~ tokens[5] ~ "|" ~ 
            ((tokens.length >= 7 && tokens[6] == "optional")?"true":"false"))));
        } else if (tokens[3] == "drop") {
          auto index =
            cast(int)countUntil!"a.getHeader().name == b"(t.getColumns(),
                                                          tokens[4]);
          if (index == -1)
            throw new Exception("There is no column named '" ~ tokens[4] ~ "'.");
          else
            t.removeColumn(index);
        } else
          throw new Exception("ADD or DROP expected instead of '" ~ tokens[3] ~ "'.");
        db.removeTable(tokens[2]);
        db.addTable(t);
        break;

      case "insert":
        if (!db)
          throw new Exception("There is no open database to operate on.");
        if (tokens[1] != "into")
          throw new Exception("The keyword INTO expected after INSERT.");
        auto table = db.getTable(tokens[2]);
        if (!table)
          throw new Exception("There is no TABLE called '" ~ tokens[2] ~ "'.");
        auto columns = table.getColumns();
        int values = 0;
        int[] row_map;
        // create rowmap
        foreach(int i, string token; tokens[3..$]) {
          if (token == "values") {
            values = i; break;
          }
          row_map ~=
            cast(int)countUntil!"a.getHeader().name == b"(columns, token);
          if (row_map[$-1] == -1)
            throw new Exception("There is no column named '" ~ token ~ "'.");
        }
        // Check if all the non optional fields are filled
        foreach(int i, Column c; columns) {
          if (!c.getHeader().optional && !count(row_map,i))
            throw new Exception("The column '" ~ c.getHeader().name ~ "' is not optional.");
        }
        // insert values
        int cols = cast(int)row_map.length;
        Row r;
        r.length = columns.length;
        int index = -1;
        try {
          foreach(int typeIndex, string token; tokens[(values+4)..$]) {
            index = typeIndex % cols;
            if (index == 0 && typeIndex != 0) {
              table.addRow(r);
              r.length = 0; r.length = columns.length; // clear rows
            }
            DataType type = columns[row_map[index]].getHeader().type;
            r[row_map[index]] = EntryFactory(token, type);
          }
          if (index == (cols-1)) table.addRow(r);
        } catch (std.conv.ConvException exc) {
          throw new Exception(exc.msg);
        }
        db.removeTable(tokens[2]);
        db.addTable(table);
        break;

      case "delete":
        if (!db)
          throw new Exception("There is no open database to operate on.");
        if (tokens[1] != "from") 
          throw new Exception("The keyword FROM expected after DELETE.");
        string table_name = tokens[2];
        auto t = db.getTable(table_name);
        if (!t) throw new Exception("The table " ~ table_name ~ " is not found.");
        auto selector = parseWhere(tokens[4..$], t);
        auto result = t.deleteRows(selector);
        write(result);
        writeln(" rows were deleted.");
        break;

      case "select":
        if (!db) throw new Exception("There is no open database to operate on.");
        bool all = false;
        if (tokens[1] == "all") all = true;
        if (tokens[1+cast(int)all] != "from")
          throw new Exception("The keyword FROM expected after SELECT.");
        string table_name = tokens[2+cast(int)all];
        auto t = db.getTable(table_name);
        if (!t) throw new Exception("The table " ~ table_name ~ " is not found.");
        if (all) {
          auto allRows = delegate (Row r) { return true; };
          foreach(Row r; t.getRows(allRows)) {
            foreach(entry; r)
              if (entry)
                write(entry.toStringRepr() ~ "\t");
            writeln();
          }
        } else {
          auto selector = parseWhere(tokens[4..$], t);
          foreach(Row r; t.getRows(selector)) {
            foreach(entry; r)
              if (entry)
                write(entry.toStringRepr() ~ "\t");
            writeln();
          }
        }
        break;
      default:
        throw new Exception("Unknown keyword: " ~ tokens[0] ~ ".");
    }
    }
    catch(RangeError) {
      throw new Exception("Parse error, probably missing keywords.");
    }
    return true;
  }

  void setStorageModell(Storage s) { stor = s; }

private:

  string[] tokenize(string input) {
    input = tr(input,","," ");
    input = tr(input,"("," ");
    input = tr(input,")"," ");
    return splitLines(replace(input, regex(r"\s+","g"), "\n"));
  }

  bool delegate(Row) parseWhere(string[] tokens, Table t) {
	bool delegate(Row) selector = delegate bool(Row r) { return false; };
    auto index =
      cast(int)countUntil!"a.getHeader().name == b"(t.getColumns(), tokens[0]);
    if(index == -1)
      throw new Exception("There is no column named: '" ~ tokens[0]  ~ "'.");
    switch (tokens[1]) {
      case "<":
		selector = delegate bool(Row r) { return r[index] && r[index] < tokens[2]; };
        break;
      case "<=":
        selector = delegate bool(Row r) { return r[index] && r[index] <= tokens[2]; };
        break;
      case "==":
        selector = delegate bool(Row r) { return r[index] && r[index] == tokens[2]; };
        break;
      case ">":
        selector = delegate bool(Row r) { return r[index] && r[index] > tokens[2]; };
        break;
      case ">=":
        selector = delegate bool(Row r) { return r[index] && r[index] >= tokens[2]; };
        break;
      default:
        throw new Exception("Unsupported operator: " ~ tokens[1]);
    }
    return selector;
  }
  
  bool[string] keywords;
  Database db;
  Storage stor;

  enum HeaderState : int {
    Named,
    Typed
  }
}
